package nz.ac.massey.groupproject.ecs2;

import java.util.ArrayList;
import java.util.HashMap;


/*
 * A World is a just pool of Entities with some bells and whistles.
 *
 * Unlike some other engines, our World is strictly Components and Entities.
 * Systems are not part of a World.  In other words, World is fact, not behavior.
 * TODO documentation: this is because different Systems might wanna process
 * different Entity pools (i.e. Worlds), but we still wanna chain these Systems
 * in a single Pipeline.
 *
 * Because a World is nothing more than a pool, an Entity may belong to multiple worlds.
 * Adding the same entity to the same world multiple times is equivalent to adding just once.
 * Removing an entity from a world it doesn't belong to has no effect.
 *
 * Entities can be referred to by pointer or by index.
 *   - pointer is never invalidated due to the nature of GC
 *   - if an entity belongs to multiple worlds, it has one index per world
 *   - index is between 0..(n'-1) where n' is >= total number of entities in that world
 *   - when an entity is removed from a world, its index is invalidated;  however,
 *     remaining entities' indices are not invalidated immediately.
 *   - remaining entities' indices are NOT invalidated until compact() is called, so you
 *     can choose to invalidate indices at a convenient time
 *
 * compact() must NOT be called during a query.  In other words, it must not be called in
 * tick() and the like.  You can call compact() in globalTick() or from outside a System.
 *
 * If an Entity is added during a query that matches it, it may or may not be matched during
 * this run.
 * If an Entity is removed during a query that would have matched it, it may or may not
 * be matched by the query.
 * Instead of calling addNow() and removeNow(), consider calling addLater() and removeLater().
 * These methods do not touch the entity list; therefore, query result will be predictable.
 * Once you are done, call commitChanges() to actually add/remove pending items.  If you are
 * using ECSSystemUtil.smartTick(), commitChanges() will be automatically called for you at
 * the end of each System.
 *
 * Similarly, if adding/removing/replacing Components causes a non-matching Entity to match,
 * or vice versa, such an operation may affect query result in unpredictable ways.
 * TODO Unlike for adding/removing Entities, we don't have a solution for adding/removing
 * Components.
 *
 * WARNING: the underlying data structure uses lazy removal.  Until compact() is called,
 *          lazy removed Entities are still referenced and not eligible for GC.
 */
public class ECSWorld {
	final HashMap<Class, ArrayList<ECSQuery>>  subscribers    = new HashMap<>();

	// If a pending add operation would add an existing element, we keep the operation anyways.
	// If a pending remove operation would remove a non-existing element, we keep the operation anyways.
	// The intention is to allow mixing addNow() and addLater().
	        final LazyArraySet<ECSEntity>   entities         = new LazyArraySet<>();
	private final ArrayList<ECSEntity>      pendingEntities  = new ArrayList<>();
	private final ArrayList<Boolean>        pendingOpIsAdd   = new ArrayList<>();

	// final is important because if globalData.getClass() may change, we can't cache Queries
	public final Object                     globalData;


	public ECSWorld()         { globalData = null; }
	public ECSWorld(Object d) { globalData = d;    }



	// Return true if added,  false if already exists.
	public boolean addNow(ECSEntity ent) {
		if (! entities.add(ent)) return false;

		ent.worlds.add(this);
		for (var comp : ent.comps)
			notifySubscribers(ECSQuery.EventType.ADD_ENTITY, ent, comp);
		return true;
	}

	// Return true if removed,  false if not in pool to begin with.
	public boolean removeNow(ECSEntity ent) {
		if (! entities.remove(ent)) return false;

		ent.worlds.remove(this);
		for (var comp : ent.comps)
			notifySubscribers(ECSQuery.EventType.REMOVE_ENTITY, ent, comp);
		return true;
	}

	public void compact() {  entities.compact(false);  }



	public void addLater(ECSEntity ent) {
		pendingEntities.add(ent);
		pendingOpIsAdd.add(true);
	}
	public void removeLater(ECSEntity ent) {
		pendingEntities.add(ent);
		pendingOpIsAdd.add(false);
	}
	public void commitChanges() {
		for (int i = 0; i < pendingEntities.size(); i++) {
			var ent = pendingEntities.get(i);
			if (pendingOpIsAdd.get(i))
				addNow(ent);
			else
				removeNow(ent);
		}
		pendingEntities.clear();
		pendingOpIsAdd.clear();
	}





	void notifySubscribers(ECSQuery.EventType event, ECSEntity ent, ECSComponent comp) {
		Class cls = comp.getClass();

		var subbers = subscribers.get(cls);
		if (null == subbers) return;
		for (var query : subbers)
			query.onEvent(event, ent, comp);
	}
}
